package com.naiterui.ehp.bs.payment.service.impl;

import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryResult;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.naiterui.ehp.bp.constants.PayStatus;
import com.naiterui.ehp.bp.utils.date.DateUtil;
import com.naiterui.ehp.bp.utils.payment.bo.OrderStatusResultBO;
import com.naiterui.ehp.bp.utils.payment.bo.RefundRequestBO;
import com.naiterui.ehp.bp.utils.payment.bo.UnifiedOrderRequestBO;
import com.naiterui.ehp.bp.utils.payment.bo.UnifiedOrderResultBO;
import com.naiterui.ehp.bs.payment.entity.PayInfo;
import com.naiterui.ehp.bs.payment.entity.RefundInfo;
import com.naiterui.ehp.bs.payment.entity.TradeFlowRecord;
import com.naiterui.ehp.bs.payment.exception.ExceptionCodes;
import com.naiterui.ehp.bs.payment.exception.NotifyRefundException;
import com.naiterui.ehp.bs.payment.exception.PayStatusException;
import com.naiterui.ehp.bs.payment.exception.PaymentBusinessException;
import com.naiterui.ehp.bs.payment.repository.IPayInfoRepository;
import com.naiterui.ehp.bs.payment.repository.IRefundInfoRepository;
import com.naiterui.ehp.bs.payment.repository.ITradeFlowRecordRepository;
import com.naiterui.ehp.bs.payment.service.INotifyBusinessService;
import com.naiterui.ehp.bs.payment.service.IOrderService;
import com.naiterui.ehp.bs.payment.service.IOrderSnService;
import com.naiterui.ehp.bs.payment.service.IOrderStatusService;
import com.naiterui.ehp.bs.payment.service.IRefundInfoService;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 支付功能相关接口实现：包括统一下单，查询订单，关闭订单，退款，回调通知接口
 * 具体接口请看接口注释
 *
 * @date 2017年09月09日
 * @since 1.0.0
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements IOrderService {

    private final WxPayService wxPayService;
    private final IOrderSnService orderSnService;
    private final IRefundInfoService refundInfoService;
    private final IOrderStatusService orderStatusService;
    private final IPayInfoRepository payInfoRepository;
    private final IRefundInfoRepository refundInfoRepository;
    private final INotifyBusinessService notifyBusinessService;
    private final ITradeFlowRecordRepository tradeFlowRecordRepository;

    @Value("${wx.pay.notify-url}")
    private String payNotifyUrl;

    /**
     * 如果查询三方返回此结果，那么认为不是错误
     */
    private final String[] isNotMistake = {"Order not exists", "Order not exists or offline"};

    /**
     * 订单状态
     */
    private final Map<String, PayStatus> payStatusMap = new HashMap<String, PayStatus>() {
        private static final long serialVersionUID = -4812233483697410345L;

        {
            this.put("SUCCESS", PayStatus.PAY_SUCCESS);
            this.put("NOTPAY", PayStatus.WAIT_PAY);
            this.put("CLOSED", PayStatus.ORDER_CLOSE);
            this.put("REFUND", PayStatus.REFUND);
            this.put("REVERSE", PayStatus.PAY_FAIL);
            this.put("REVOK", PayStatus.PAY_FAIL);
        }
    };

    @Override
    @Transactional(rollbackFor = Exception.class)
    public UnifiedOrderResultBO unifiedOrder(final UnifiedOrderRequestBO request) throws WxPayException {
        log.info("统一下单接口 {}", request);
        long start = System.currentTimeMillis();

        // 获取预信息
        PayInfo payInfo = this.getPayInfoTradeSn(request);
        // 将订单号换成流水号,因为这里使用流水号支付
        request.setTradeSn(payInfo.getTradeSn());
        WxPayUnifiedOrderRequest buiWxPayUnifiedOrderRequestld = WxPayUnifiedOrderRequest.newBuilder()
                                                                                         // 商品描述
                                                                                         .body(request.getBody())
                                                                                         // 订单金额
                                                                                         .totalFee(request.getTotalFee())
                                                                                         // 生成IP
                                                                                         .spbillCreateIp(request.getIp())
                                                                                         // 订单号
                                                                                         .outTradeNo(StringUtils.defaultIfBlank(request.getTradeSn(),
                                                                                                                                request.getOrderSn()))
                                                                                         // 设备号
                                                                                         .deviceInfo(request.getDeviceInfo())
                                                                                         .tradeType(WxPayConstants.TradeType.JSAPI)
                                                                                         .notifyUrl(this.payNotifyUrl)
                                                                                         // 用户openid
                                                                                         .openid(request.getOpenId()).build();
        WxPayMpOrderResult wxPayMpOrderResult = this.wxPayService.createOrder(buiWxPayUnifiedOrderRequestld);
        payInfo.setChannelType(1);
        this.payInfoRepository.update(payInfo);
        log.info("统一下单接口处理完成 cost:{}, orderSn:{}", System.currentTimeMillis() - start, request.getOrderSn());
        return UnifiedOrderResultBO.builder()
                                   .orderSn(payInfo.getOrderSn())
                                   .tradeSn(payInfo.getTradeSn())
                                   .appId(wxPayMpOrderResult.getAppId())
                                   .timeStamp(wxPayMpOrderResult.getTimeStamp())
                                   .nonceStr(wxPayMpOrderResult.getNonceStr())
                                   .packageStr(wxPayMpOrderResult.getPackageValue())
                                   .signType(wxPayMpOrderResult.getSignType())
                                   .paySign(wxPayMpOrderResult.getPaySign())
                                   .build();
    }

    /**
     * 获取支付信息，如果没有支付信息，那么就先创建
     *
     * @param request 创建预支付信息
     *
     * @return 支付流水单号
     */
    private PayInfo getPayInfoTradeSn(UnifiedOrderRequestBO request) throws WxPayException {
        String orderSn = request.getOrderSn();
        Integer origin = request.getOrigin();
        Integer orderType = request.getOrderType();
        PayInfo payInfo = this.payInfoRepository.findByOrderSn(orderSn);
        if (payInfo == null) {
            long totalFee = request.getTotalFee().longValue();
            // 支付单号
            String tradeSn = this.orderSnService.getPayInfoTradeSn(orderType);
            return this.payInfoRepository.save(PayInfo.builder()
                                                      // 业务订单号
                                                      .orderSn(orderSn)
                                                      // 订单金额
                                                      .price(totalFee)
                                                      // 订单类型
                                                      .type(orderType)
                                                      // 商品名称
                                                      .productName(request.getBody())
                                                      // 用户标识
                                                      .userId(request.getOpenId())
                                                      // 默认订单状态
                                                      .status(PayStatus.WAIT_PAY.getValue())
                                                      // 业务回调地址
                                                      .notifyBusinessUrl(request.getNotifyBusinessUrl())
                                                      .notifyBusinessStatus(0)
                                                      .createdAt(new Date())
                                                      .createdBy("system")
                                                      .changedAt(new Date())
                                                      .changedBy("system")
                                                      // 订单平台
                                                      .origin(origin)
                                                      // 支付单号
                                                      .tradeSn(tradeSn).build());
        }
        // 如果订单表的状态不是待支付，证明已经支付过或者已经退款，所以不能再次发起支付
        Integer payInfoStatus = payInfo.getStatus();
        if (PayStatus.WAIT_PAY.getValue() != payInfoStatus) {
            throw new PayStatusException(String.format("订单号状态不正确，不能创建预支付。orderSn:%s 订单状态:%s", orderSn, payInfoStatus));
        }
        // 这里关单
        this.remoteCloseOrder(payInfo.getTradeSn());
        // 保存该订单的支付流水记录
        this.tradeFlowRecordRepository.save(TradeFlowRecord.builder()
                                                           // 业务订单号
                                                           .orderSn(orderSn)
                                                           // 支付平台
                                                           .origin(payInfo.getOrigin())
                                                           // 用户标识
                                                           .userId(payInfo.getUserId())
                                                           // 原因
                                                           .message(this.getTradeFlowRecordMessage(payInfo, request))
                                                           // 支付流水号
                                                           .tradeSn(payInfo.getTradeSn())
                                                           .createdAt(new Date())
                                                           .createdBy("system")
                                                           .changedAt(new Date())
                                                           .changedBy("system").build());
        // 支付单号
        payInfo.setTradeSn(this.orderSnService.getPayInfoTradeSn(orderType));
        payInfo.setOrigin(origin);
        payInfo.setType(orderType);
        payInfo.setProductName(request.getBody());
        payInfo.setStatus(PayStatus.WAIT_PAY.getValue());
        payInfo.setPrice(request.getTotalFee().longValue());
        payInfo.setNotifyBusinessUrl(request.getNotifyBusinessUrl());
        return payInfo;
    }

    /**
     * 获取变更信息
     *
     * @param payInfo 老的支付信息
     * @param request 新的支付请求信息
     *
     * @return 变更信息
     */
    private String getTradeFlowRecordMessage(final PayInfo payInfo, final UnifiedOrderRequestBO request) {
        StringBuilder message = new StringBuilder();
        Long price = payInfo.getPrice();
        if (price == null || price.intValue() != request.getTotalFee()) {
            message.append(String.format("订单金额变更，原始金额:%d 变更金额:%d ", price, request.getTotalFee()));
        }
        String productName = payInfo.getProductName();
        if (productName == null || !productName.equals(request.getBody())) {
            message.append(String.format("商品名称变更，原始名称:%s 变更名称:%s ", productName, request.getBody()));
        }
        Integer origin = payInfo.getOrigin();
        if (origin == null || !origin.equals(request.getOrigin())) {
            message.append(String.format("订单来源变更，原始来源:%d 变更来源:%d ", origin, request.getOrigin()));
        }
        String notifyBusinessUrl = payInfo.getNotifyBusinessUrl();
        if (StringUtils.isBlank(notifyBusinessUrl) || !notifyBusinessUrl.equals(request.getNotifyBusinessUrl())) {
            message.append("订单通知地址变更 ");
        }
        if (StringUtils.isBlank(message.toString())) {
            message.append("用户再次点击支付");
        }
        return message.toString();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public OrderStatusResultBO queryOrderPayStatus(final String orderSn) throws PaymentBusinessException, WxPayException {
        log.info("查询订单状态 orderSn:{}", orderSn);
        PayInfo payInfo = this.getPayInfoByOrderSn(orderSn);
        // 订单状态如果不是待支付状态，那么直接返回订单状态
        Integer status = payInfo.getStatus();
        if (PayStatus.WAIT_PAY.getValue() != status) {
            log.debug("订单状态不是待支付，直接返回订单状态，orderSn{}", orderSn);
            return new OrderStatusResultBO(orderSn, status);
        }
        PayStatus payStatus;
        try {
            //优先tradeSn查询
            WxPayOrderQueryResult wxPayOrderQueryResult = this.wxPayService.queryOrder(Optional.ofNullable(payInfo.getTradeSn()).map(tradeSn -> "").orElse(orderSn), payInfo.getTradeSn());
            String tradeState = wxPayOrderQueryResult.getTradeState();
            payStatus = this.payStatusMap.get(StringUtils.upperCase(tradeState));
            if (payStatus == null) {
                log.warn("订单状态不正确 orderSn: {} tradeState:{}", orderSn, tradeState);
                throw new PayStatusException(String.format("订单状态不正确orderSn: %s tradeState:%s", orderSn, tradeState));
            }
        } catch (WxPayException e) {
            if (!StringUtils.equalsAny(e.getErrCode(), this.isNotMistake)) {
                throw e;
            }
            log.warn("订单不存在，或称为历史订单，认为订单状态为待支付。orderSn:{} tradeSn:{}", orderSn, payInfo.getTradeSn());
            return new OrderStatusResultBO(orderSn, PayStatus.WAIT_PAY.getValue());
        }
        // 如果查询到的是支付成功或者支付失败，那么更新成支付中状态，
        if (PayStatus.PAY_SUCCESS.equals(payStatus) || PayStatus.PAY_FAIL.equals(payStatus)) {
            this.payInfoRepository.updateStatus(orderSn, PayStatus.PAY_NOT_CONFIRMED.getValue());
        }
        return new OrderStatusResultBO(orderSn, payStatus.getValue());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void closeOrder(final String orderSn) throws PaymentBusinessException, WxPayException {
        log.info("调用关单接口，orderSn:{}", orderSn);
        PayInfo payInfo = this.payInfoRepository.findByOrderSn(orderSn);
        if (payInfo == null) {
            return;
        }
        Integer status = payInfo.getStatus();
        if (PayStatus.ORDER_CLOSE.getValue() == status) {
            log.debug("订单已经关闭，不在请求接口。orderSn:{}", orderSn);
            return;
        }
        // 如果不是待支付的订单，那么直接返回，不在调用接口
        if (PayStatus.WAIT_PAY.getValue() != status) {
            throw new PayStatusException(String.format("订单状态不正确，不能关单，订单号：%s", orderSn));
        }
        // 支付中的订单是可以调用关单接口的
        this.remoteCloseOrder(payInfo.getTradeSn());
        // 将订单设置成关单
        this.payInfoRepository.updateStatus(orderSn, PayStatus.ORDER_CLOSE.getValue());
    }

    private void remoteCloseOrder(String tradeSn) throws WxPayException {
        try {
            this.wxPayService.closeOrder(tradeSn);
        } catch (WxPayException e) {
            if (!StringUtils.equalsAny(e.getErrCode(), this.isNotMistake)) {
                throw e;
            }
            log.warn("订单不存在，不需要关单。 tradeSn:{}", tradeSn);
        }
    }

    /**
     * 此接口为退款接口需要注意 事物隔离级别为READ_COMMITTED，因为调用 orderSnService
     * 生成退款信息，此退款信息生成之后必须提交事务，因为如果调用退款接口失败的话，必须使用原来退款信息重新调用
     *
     * @param request 退款信息
     *
     * @throws PaymentBusinessException 支付信息不存在
     */
    @Async
    @Override
    @Transactional(rollbackFor = Exception.class)
    @Retryable(value = NotifyRefundException.class, maxAttempts = 5, backoff = @Backoff(multiplier = 2, random = true))
    public void refund(final RefundRequestBO request) throws PaymentBusinessException, NotifyRefundException, WxPayException {
        RefundInfo refundInfo = this.refundInfoService.getRefundInfo(request);

        try {
            this.wxPayService.refund(WxPayRefundRequest.newBuilder()
                                                       // 商户订单号
                                                       .outTradeNo(refundInfo.getTradeSn())
                                                       // 商户退款单号
                                                       .outRefundNo(refundInfo.getRefundOrderSn())
                                                       // 订单金额
                                                       .totalFee(refundInfo.getPrice())
                                                       // 退款金额
                                                       .refundFee(refundInfo.getPrice()).build());
        } catch (WxPayException e) {
            log.error("退款异常啦！！ e {}", e);
            log.error("退款异常啦！！ 1-{}, 2-{}, 3-{}, 4-{}, 5-{}, 6-{}, 7-{}", e.getErrCode(), e.getErrCodeDes(), e.getResultCode(), e.getReturnCode(), e.getReturnMsg(), e.getCustomErrorMsg(), e.getXmlString());
            if (e.getErrCodeDes().equalsIgnoreCase("订单已全额退款")) {
                throw e;
            }
            throw new NotifyRefundException(String.format("支付流水单号：%s 退款失败", refundInfo.getTradeSn()), e.getErrCode(), e.getErrCodeDes(), e);
        }
        // 判断金额，更新成部分退款
        this.payInfoRepository.updateStatus(refundInfo.getOrderSn(), PayStatus.REFUND.getValue());
        //更新退款单退款状态
        this.refundInfoRepository.updateStatus(refundInfo.getRefundOrderSn(), RefundInfo.ORDER_STATUS_SUCCEED);
        // 通知业务
        this.notifyBusinessService.notifyBusinessRefund(refundInfo.getRefundOrderSn());
    }

    /**
     * 重试退款失败，需要更新退款状态和错误信息，然后在通知业务系统
     *
     * @param notifyRefundException 退款异常
     */
    @Recover
    public void recoverNotifyRefundException(NotifyRefundException notifyRefundException, final RefundRequestBO request) {
        String refundOrderSn = request.getRefundOrderSn();
        log.error("退款订单号：{} 退款失败", refundOrderSn, notifyRefundException);
        this.orderStatusService.updateRefund(refundOrderSn, notifyRefundException.getErrCode(), notifyRefundException.getErrCodeDes());
        // 退款失败，需要通知业务
        this.notifyBusinessService.notifyBusinessRefund(refundOrderSn);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void notify(String xml) throws PaymentBusinessException, WxPayException {
        WxPayOrderNotifyResult wxPayOrderNotifyResult = this.wxPayService.parseOrderNotifyResult(xml);
        String tradeSn = wxPayOrderNotifyResult.getOutTradeNo();
        // 订单号
        PayInfo payInfo = this.payInfoRepository.findByTradeSn(tradeSn);
        String orderSn = payInfo.getOrderSn();
        // 判断订单状态
        Integer status = payInfo.getStatus();
        if (PayStatus.WAIT_PAY.getValue() != status && PayStatus.PAY_NOT_CONFIRMED.getValue() != status) {
            log.warn("订单状态不是待支付，不能更新订单状态。支付流水号：{} 订单当前状态：{}", tradeSn, status);
            throw new PayStatusException(String.format("订单状态不是待支付，不能更新订单状态。支付流水号:%s 订单状态：%s", tradeSn, status));
        }
        // 默认支付成功
        payInfo.setStatus(PayStatus.PAY_SUCCESS.getValue());
        // 判断支付金额和订单金额是否一致
        Integer totalFee = wxPayOrderNotifyResult.getTotalFee();
        Long price = payInfo.getPrice();
        if (totalFee != price.intValue()) {
            log.warn("支付金额和订单金额不一致，支付流水号:{} 支付金额：{} 订单金额：{} ", tradeSn, totalFee, price);
            payInfo.setStatus(PayStatus.PAY_FAIL.getValue());
            payInfo.setFailMsg(String.format("支付金额和订单金额不一致，支付金额：%s 订单金额：%s", totalFee, price));
        }
        // 上游流水号
        payInfo.setChannelTradeSn(wxPayOrderNotifyResult.getTransactionId());
        // 支付完成时间
        payInfo.setPayTime(DateUtil.parseStrToCustomPatternDate(wxPayOrderNotifyResult.getTimeEnd(), DateUtil.DATE_TIME_FORMAT_MIN));
        this.payInfoRepository.update(payInfo);
        // 通知业务系统
        this.notifyBusinessService.notifyBusinessPay(orderSn);
    }

    /**
     * 通过订单号获取系统支付信息
     *
     * @param orderSn 商户系统内部的订单号 不能为空
     *
     * @return 系统支付信息
     *
     * @throws PaymentBusinessException 订单没查询到
     */
    private PayInfo getPayInfoByOrderSn(String orderSn) throws PaymentBusinessException {
        PayInfo payInfo = this.payInfoRepository.findByOrderSn(orderSn);
        if (payInfo == null) {
            log.warn("订单号没有查询到，orderSn:{}", orderSn);
            throw new PaymentBusinessException(ExceptionCodes.PAY_ORDER_NOT_FIND);
        }
        return payInfo;
    }
}
